/**
 * @file disk.c
 * @author lcz (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2022-11-10
 *
 * @copyright Copyright (c) 2022
 *
 */


#include "dev/disk.h"
#include "comm/cpu_instr.h"
#include "comm/boot_info.h"
#include "tools/log.h"
#include "tools/kilb.h"
#include "dev/dev.h"
#include "cpu/irq.h"
#include "core/task.h"
// 当前磁盘个数 2
static disk_t disk_buf[DISK_CNT];
// 进程读取导致中断，标志位
static int task_on_op = 0;
static mutex_t mutex;
static sem_t op_sem;

/**
 * @brief 发送磁盘命令
 * @param disk 磁盘
 * @param start_sector 开始商区
 * @param sector_count 扇区个数
 * @param cmd 命令
 */
static void
disk_send_cmd(disk_t *disk, uint32_t start_sector, uint32_t sector_count, int cmd)
{
    outb(DISK_DRIVE(disk), DISK_DRIVE_BASE | disk->drive); // 使用LBA寻址，并设置驱动器

    // 必须先写高字节
    outb(DISK_SECTOR_COUNT(disk), (uint8_t)(sector_count >> 8)); // 扇区数高8位
    outb(DISK_LBA_LO(disk), (uint8_t)(start_sector >> 24));      // LBA参数的24~31位
    outb(DISK_LBA_MID(disk), 0);                                 // 高于32位不支持
    outb(DISK_LBA_HI(disk), 0);                                  // 高于32位不支持

    outb(DISK_SECTOR_COUNT(disk), (uint8_t)(sector_count)); // 扇区数量低8位
    outb(DISK_LBA_LO(disk), (uint8_t)(start_sector >> 0));  // LBA参数的0-7
    outb(DISK_LBA_MID(disk), (uint8_t)(start_sector >> 8)); // LBA参数的8-15位
    outb(DISK_LBA_HI(disk), (uint8_t)(start_sector >> 16)); // LBA参数的16-23位

    // 选择对应的主-从磁盘
    outb(DISK_CMD(disk), (uint8_t)cmd);
}
/**
 * @brief 指定disk磁盘读取指定size字节到buf缓冲区
 */
static void disk_read_data(disk_t *disk, void *buf, int size)
{
    // 我们一次读取两个字节
    uint16_t *c = (uint16_t *)buf;
    for (int i = 0; i < size / 2; i++)
    {
        *c++ = inw(DISK_DATA(disk));
    }
}
/**
 * @brief 指定disk磁盘写size直接，写的数据来源地址为buf
 */
static void disk_write_data(disk_t *disk, void *buf, int size)
{
    // 我们一次读取两个字节
    uint16_t *c = (uint16_t *)buf;
    for (int i = 0; i < size / 2; i++)
    {
        outw(DISK_DATA(disk), *c++);
    }
}

/**
 * @brief 磁盘等待，等待磁盘disk完成指定操作
 * 我们的数据读取完了 我们退出, 读取磁盘错误 发生错误我们也退出
 * @param disk
 * @return * int 放回-1代表发生错误的退出 0表示正常
 */
static int disk_wait_data(disk_t *disk)
{
    uint8_t status;
    do
    {
        status = inb(DISK_STATUS(disk));
        // if 简写了 结果不为busy 我们认为结束了
        if ((status & (DISK_STATUS_BUSY | DISK_STATUS_DRQ | DISK_STATUS_ERR)) != DISK_STATUS_BUSY)
        {
            break;
        }
    } while (1);
    return (status & DISK_STATUS_ERR) ? -1 : 0;
}
/**
 * @brief 答应disk磁盘信息，包括名称，端口基地址，磁盘大小（M）
 * @param disk
 */
static void print_disk_info(disk_t *disk)
{
    log_printf("%s", disk->name);
    // 根据地址我们就可以判断是主总线还是次总线
    log_printf("  port base: %x", disk->port_base);
    // 磁盘多少兆
    log_printf("  total size: %d m", disk->sector_count * disk->sector_size / 1024 / 1024);

    for (int i = 0; i < DISK_PAIMARY_PART_CNT; i++)
    {
        partinfo_t *part_info = disk->partinfo + i;
        if (part_info->type != FS_INVALID)
        {
            log_printf("    %s:type: %x, start sector: %d, count: %d", part_info->name, part_info->type, part_info->start_sector, part_info->total_sector);
        }
    }
}
/**
 * @brief 给定磁盘disk获取分区信息
 * @return * int
 */
static int detect_part_info(disk_t *disk)
{
    mbr_t mbr;
    // 读取磁盘的第一个扇区，里面记录了分区的情况,读取分区表请款信息 512字节
    disk_send_cmd(disk, 0, 1, DISK_CMD_READ);
    int err = disk_wait_data(disk);
    if (err < 0)
    {
        log_printf("read mbr fialed.");
        return err;
    }
    // 读取 MBR区域描述结构
    disk_read_data(disk, &mbr, sizeof(mbr));
    // 分区表指针
    part_item_t *item = mbr.part_item; // 磁盘第一扇区地分区表指针
    partinfo_t *part_info = disk->partinfo + 1;
    // 遍历分区表 表项信息复制到disk中的partinfo结构中，partinfo对应一个分区
    for (int i = 0; i < MBR_PRIMARY_PART_NR; i++, item++, part_info++)
    {
        // 获取分区表地系统id，为0无系统，不对disk分区表项初始化
        part_info->type = item->system_id;
        // system_id为0时表示文件系统不可用，复制后type为0，及等于FS_INCALID
        if (part_info->type == FS_INVALID)
        {
            part_info->total_sector = 0;
            part_info->start_sector = 0;
            part_info->disk = (disk_t *)0;
        }
        else
        {
            kernel_printf(part_info->name, "%s%d", disk->name, i + 1);

            part_info->start_sector = item->relative_sectors;
            part_info->total_sector = item->total_sectors;
            part_info->disk = disk;
        }
    }
}
/**
 * @brief 磁盘识别函数，实际上该函数调用以上的函数完成磁盘的检测及disk结构体的
 * 初始化工作
 * @param disk 磁盘
 * @return * int  正常放回0 异常-1
 */
static int identify_disk(disk_t *disk)
{
    // 0 0 开始扇区 扇区数量 DISK_IDENTIFY命令
    disk_send_cmd(disk, 0, 0, DISK_CMD_IDENTIFY);
    int err = inb(DISK_STATUS(disk));
    if (err = 0)
    {
        log_printf("%s doesn't exist", disk->name);
        return -1;
    }

    err = disk_wait_data(disk);

    if (err < 0)
    {
        log_printf("disk[%s]: read failed", disk->name);
        return -1;
    }

    // 现在我们开始校验读取的数据
    uint16_t buf[256];
    disk_read_data(disk, buf, sizeof(buf));
    // 取磁盘的数量 取64位中的一半，只读取32位
    disk->sector_count = *(uint32_t *)(buf + 100);
    disk->sector_size = SECTOR_SIZE;

    // 检测到磁盘后初始化第一个分区表
    // 分区描述的是磁盘的一块区域
    // 我们将个分区的信息放在分区表的第0项 方便管理
    partinfo_t *part = disk->partinfo + 0;
    part->disk = disk;
    kernel_printf(part->name, "%s%d", disk->name, 0);
    // 开始分区为0 总分区嘛
    part->start_sector = 0;
    part->total_sector = disk->sector_count;
    part->type = FS_INVALID;

    detect_part_info(disk);
    return 0;
}
/**
 * @brief 磁盘初始化
 */
void disk_init(void)
{
    log_printf("Check disk...");
    kernel_memset(disk_buf, 0, sizeof(disk_buf));
    mutex_init(&mutex);
    sem_init(&op_sem, 0);
    // 遍历磁盘查找
    for (int i = 0; i < DISK_PER_CHANNEL; i++)
    {
        disk_t *disk = disk_buf + i;
        // sd 开头 我们要对磁盘命名
        kernel_printf(disk->name, "sd%c", i + 'a');
        disk->drive = (i == 0) ? DISK_MASTER : DISK_SLAVE;
        disk->port_base = IOBASE_PRIMARY;
        // 实际上我们两个磁盘使用同一个锁，对的，两个，一条主总线两个磁盘
        disk->mutex = &mutex;
        disk->sem = &op_sem;
        // 我们开始识别硬盘，然后
        // 发命令 等待 读取数据
        int err = identify_disk(disk);
        if (err == 0)
        {
            print_disk_info(disk);
        }
    }

    // 安装中断
    irq_install(IRQ14_HARDDISK_PRIMARY, (irq_handler_t)exception_handler_ide_primary);
    // 开启中断
    irq_enable(IRQ14_HARDDISK_PRIMARY);
}
/**
 * @brief 磁盘中断处理程序
 *
 */
void do_handler_ide_primary(exception_frame_t *frame)
{
    // 发送中断处理完成
    pic_send_eoi(IRQ14_HARDDISK_PRIMARY);
    // 避免读磁盘的中断绕回及空进程等待
    if (task_on_op == 1 && task_current())
    {
        sem_notify(&op_sem);
    }
}


int disk_open(device_t *dev){
    // 0xa0 -- a 磁盘编号a,bc, 0 -- 分区号1，2，3
    // 我们在此设备号这里获取所有地磁盘信息，包括磁盘标号及分区编号
    // 获取磁盘号
    int disk_idx = (dev->minor >> 4) - 0xa;
    // 获取分区号
    int part_idx = dev->minor & 0xf;
    // 磁盘及分区合法性校验
    if ((disk_idx >= DISK_CNT) || (part_idx >= DISK_PAIMARY_PART_CNT))
    {
        log_printf("device minor error: %d", dev->minor);
        return -1;
    }
    // 获取磁盘
    disk_t *disk = disk_buf + disk_idx;
    if (disk->sector_count == 0)
    {
        log_printf("disk not exit,device: sd%x", dev->minor);
        return -1;
    }
    // 获取分区
    partinfo_t *part_info = disk->partinfo + part_idx;
    if (part_info->total_sector == 0)
    {
        log_printf("part not exist, device: sd%x", dev->minor);
        return -1;
    }
    // 我们将分区的信息保存到data中，后面我们可以取，用强制类型转换成相应结构体
    dev->data = part_info;

   

    return 0;
}
/**
 * @brief 读取磁盘
 * 进程读取磁盘的过程中，进程发送读磁盘数据后，就进入等待，等到磁盘中断到来时再唤醒该进程 同时我们要加互斥锁
 * @param dev 设备
 * @param addr 起始扇区
 * @param buf 缓冲区，读取的数据放在这里
 * @param size 读取的扇区个数
 * @return * int 放回这圈读取的扇区个数
 */
int disk_read(device_t *dev, int addr, char *buf, int size)
{
    // 获取扇区
    partinfo_t *part_info = (partinfo_t *)dev->data;
    if (!part_info)
    {
        log_printf("Get part info failed,device: %d", dev->minor);
        return -1;
    }
    // 获取磁盘
    disk_t *disk = part_info->disk;
    if (disk == (disk_t *)0)
    {
        log_printf("No disk for device %d", dev->minor);
        return -1;
    }

    mutex_lock(disk->mutex);
    task_on_op = 1;
    // 对分区的相对位移读写
    disk_send_cmd(disk, part_info->start_sector + addr, size, DISK_CMD_READ);
    int cnt;
    // 一次仅可以读取一个磁盘的扇区大小
    for (cnt = 0; cnt < size; cnt++, buf += disk->sector_size)
    {
        // 没有进程运行，初始化时不等，等不来的
        if (task_current())
        {
            sem_wait(disk->sem);
        }
       
        int err = disk_wait_data(disk);
        if (err < 0)
        {
            log_printf("disk(%s) read error: start sector %d,count: %d", disk->name, addr, size);
            break;
        }

        disk_read_data(disk, buf, disk->sector_size);
    }
    mutex_unlock(disk->mutex);
    // 单位为扇区
    return cnt;
}
/**
 * @brief 写磁盘
 * 进程读取磁盘的过程中，进程发送读磁盘数据后，就进入等待，等到磁盘中断到来时再唤醒该进程 同时我们要加互斥锁
 * @param dev 设备
 * @param addr 起始扇区
 * @param buf 缓冲区，读取的数据放在这里
 * @param size 写的扇区个数
 * @return * int 放回这圈读取的扇区个数
 */
int disk_write(device_t *dev, int addr, char *buf, int size)
{
    // 获取扇区
    partinfo_t *part_info = (partinfo_t *)dev->data;
    if (!part_info)
    {
        log_printf("Get part info failed,device: %d", dev->minor);
        return -1;
    }
    // 获取磁盘
    disk_t *disk = part_info->disk;
    if (disk == (disk_t *)0)
    {
        log_printf("No disk for device %d", dev->minor);
        return -1;
    }

    mutex_lock(disk->mutex);
    task_on_op = 1;
    // 对分区的相对位移读写
    disk_send_cmd(disk, part_info->start_sector + addr, size, DISK_CMD_WRITE);
    int cnt;
    // 一次仅可以读取一个磁盘的扇区大小
    for (cnt = 0; cnt < size; cnt++, buf += disk->sector_size)
    {
        disk_write_data(disk, buf, disk->sector_size);
        if (task_current())
        {
            sem_wait(disk->sem);
        }
        
        int err = disk_wait_data(disk);
        if (err < 0)
        {
            log_printf("disk(%s) read error: start sector %d,count: %d", disk->name, addr, size);
            break;
        }
    }
    mutex_unlock(disk->mutex);
    // 单位为扇区
    return cnt;
}
int disk_control(device_t *dev, int cmd, int arg0, int arg1)
{
    return -1;
}
int disk_close(device_t *dev)
{
    return 0;
}



dev_desc_t dev_disk_desc = {
    .name = "tty",
    .major = DEV_DISK,
    .open = disk_open,
    .read = disk_read,
    .write = disk_write,
    .control = disk_control,
    .close = disk_close,
};
